#include "backupengine.h"
#include "dbengine.h"
#include "fileinfo.h"
#include "fileutils.h"
#include "backuptask.h"
#include "restoretask.h"
#include "XDeltaWrapper.h"
#include <typeinfo>

BackupEngine::BackupEngine()
{
    timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, &BackupEngine::checkAvailableTasks);
    timer->setSingleShot(true);

    shouldSuspend = false;
    shouldStop = false;
}

BackupEngine::~BackupEngine()
{
    this->thread()->deleteLater();
}

void BackupEngine::checkPendingTasks()
{
    if(this->activeTask.isNull() && !taskQueue.isEmpty())
    {
        activeTask = taskQueue.dequeue();

        if(activeTask->getTaskType() == BackupTask::Backup) {
            doBackup((activeTask));
        } else {
            doRestore(qSharedPointerCast<RestoreTask>(activeTask));
        }

        QCoreApplication::processEvents();

        activeTask.clear();
        checkPendingTasks();
    }
}

void BackupEngine::trySuspend()
{
    while(shouldSuspend)
    {
        suspendCondition.wait(&suspendMutex);
    }
}

BackupEngine::EngineError BackupEngine::tryStop()
{
    if(shouldStop) {
        shouldStop = false;
        emit this->progressMaximumChanged(0);
        emit this->progressValueAdd(0);
        emit this->progressMessageChanged(tr("Backup canceled."));
        return BackupEngine::Canceled;
    } else {
        return BackupEngine::OK;
    }
}

void BackupEngine::doBackup(QSharedPointer<BackupTask> task)
{
    BackupEngine::EngineError result;
    emit this->changeCurrentTab(1);

    quint32 instance_parent = 0;
    QDateTime timestamp = QDateTime::currentDateTime();
    QString sourcePath = task->getSourcePath();
    QString targetPath = QString("%1/backup/%2/%3")
            .arg(task->getTargetPath())
            .arg(timestamp.toTime_t())
            .arg(sourcePath.split('/').last());
    QString diffPath = "";

    if(task->getBackupType() == BackupTask::Differential) {
        //Determine if exists any parent instance
        QList<QDateTime> instances = task->getInstances();
        foreach(QDateTime instance, instances) {
            if(task->getInstanceParent(instance) == 0 ) {
                instance_parent = instance.toTime_t();
                qDebug() << "Instance parent: " << instance_parent;
            }
        }
        if(instance_parent) {
            diffPath = QString("%1/backup/%2/%3")
                    .arg(task->getTargetPath())
                    .arg(instance_parent)
                    .arg(sourcePath.split('/').last());
        }
    }
    qDebug() << "sourcePath: " << sourcePath;
    qDebug() << "targetPath: " << targetPath;
    qDebug() << "diffPath: " << diffPath;

    AbstractTargetAdapter *adapter;

    if(task->getTargetType() == BackupTask::Local) {
        adapter = new LocalTargetAdapter(sourcePath, targetPath, diffPath);
    } else {
        qDebug() << "Adapter not implemented...";
    }

    //Backup type
    if(task->getBackupType() == BackupTask::Complete) {
        task->saveInstance(timestamp, 0);
        result = this->backupFilesOnly(*adapter);
    } else if(task->getBackupType() == BackupTask::Differential) {
        task->saveInstance(timestamp, instance_parent);
        if(instance_parent == 0) {
            //complete backup with table
            result = this->doMainBackup(*adapter, task->getVerificationType());
        } else {
            //incremental backup with table
            result = this->doDifferentialBackup(*adapter, task->getVerificationType());
        }
    } else if(task->getBackupType() == BackupTask::Incremental) {
        task->saveInstance(timestamp, instance_parent);
        if(instance_parent == 0) {
            //complete backup with table
            result = this->doMainBackup(*adapter, task->getVerificationType());
        } else {
            //incremental backup with table
            result = this->doDifferentialBackup(*adapter, task->getVerificationType());
        }
    }

    if(result == BackupEngine::OK) {
        task->setInstanceSize(timestamp, FileUtils::getDirSize(targetPath));
    } else if(result == BackupEngine::Canceled) {
        task->deleteInstance(timestamp);
    }

    delete adapter;

    emit this->backupFinished();
}

void BackupEngine::doRestore(QSharedPointer<RestoreTask> task)
{
    emit this->changeCurrentTab(1);

    quint32 instance_parent = 0;
    QString sourcePath = QString("%1/backup/%2/%3")
            .arg(task->getTargetPath())
            .arg(task->getTimestamp().toTime_t())
            .arg(task->getSourcePath().split('/').last());
    QString targetPath = task->getRestoreTargetPath();
    QString diffPath = "";

    if(task->getBackupType() == BackupTask::Incremental) {
        quint32 instance_parent = task->getInstanceParent(task->getTimestamp());
        if(instance_parent) {
            diffPath = sourcePath.replace(QString("%1").arg(task->getTimestamp().toTime_t()),
                                          QString("%1").arg(instance_parent));
        }
    }

    LocalTargetAdapter adapter(sourcePath, targetPath, diffPath);

    //Resolve type of backup
    if(task->getBackupType() == BackupTask::Complete) {
        this->restoreFilesOnly(adapter);
    } else if(task->getBackupType() == BackupTask::Incremental) {
        if(instance_parent == 0) {
            //complete restore with table
            this->doMainRestore(adapter, task->getVerificationType());
        } else {
            this->doIncrementalRestore(adapter, task->getVerificationType());
        }
    }
}

void BackupEngine::toggleSuspend()
{
    qDebug() << "trying to suspend";
    suspendMutex.lock();
    if(!shouldSuspend)
    {
        shouldSuspend = true;
    }
    else
    {
        shouldSuspend = false;
        suspendCondition.wakeOne();
    }
    suspendMutex.unlock();
}

void BackupEngine::invokeStop()
{
    if(!activeTask.isNull()) {
        this->shouldStop = true;
    }
}

void BackupEngine::checkAvailableTasks()
{
    if(timer->isActive()) {
        timer->stop();
    }
    quint32 timeout = (1000*60*60); //1 hour

    QList<quint32> tasks = DBEngine::loadTaskIDs();
    foreach(quint32 task_id, tasks)
    {
        QSharedPointer<BackupTask> task = QSharedPointer<BackupTask>(new BackupTask(task_id));

        if(task->getPeriodic() == true) {
            if(task->getNextRun() < QDateTime::currentDateTime()) {
                task->SetupNewNextRun();
                performTask(task);
            } else if((task->getNextRun().toMSecsSinceEpoch() -
                       QDateTime::currentDateTime().toMSecsSinceEpoch()) <
                      timeout) {
                timeout = (task->getNextRun().toMSecsSinceEpoch() -
                           QDateTime::currentDateTime().toMSecsSinceEpoch());
            }
        }
    }
    timer->start(timeout);
}

void BackupEngine::performTask(QSharedPointer<BackupTask> task)
{
    qDebug() << __FILE__
             << __LINE__
             << "Enqueing backup task with ID: "
             << task->getTaskId();
    taskQueue.enqueue(task);

    checkPendingTasks();
}

BackupEngine::EngineError BackupEngine::backupFilesOnly(AbstractTargetAdapter &adapter)
{
    BackupEngine::EngineError result = BackupEngine::OK;
    QScopedPointer<QDirIterator>it(adapter.getFilesInSource());

    emit this->progressMessageChanged(tr("Backing up files from directory ") + adapter.getSourcePath() + ".");
    emit this->progressMaximumChanged(adapter.getDirSize(adapter.getFilesInSource()));

    QString fileName;
    while(it->hasNext())
    {
        it->next();
        fileName = it->fileInfo().absoluteFilePath().replace(adapter.getSourcePath() + "/", "");

        emit this->newMessage(tr("Backing up ") + fileName);

        adapter.backupFile(fileName);

        emit this->progressValueAdd(it->fileInfo().size());
        qDebug() << it->fileInfo().size();

        trySuspend();
        result = tryStop();
        if(result != BackupEngine::OK) {
            return result;
        }
    }
    emit this->progressMessageChanged(tr("Backup complete."));
    return result;
}

BackupEngine::EngineError BackupEngine::restoreFilesOnly(AbstractTargetAdapter &adapter)
{
    BackupEngine::EngineError result = BackupEngine::OK;

    QScopedPointer<QDirIterator>it(adapter.getFilesInSource());

    emit this->progressMessageChanged(tr("Restoring files to directory ") + adapter.getTargetPath() + ".");
    emit this->progressMaximumChanged(adapter.getDirSize(adapter.getFilesInSource()));

    QString fileName;
    while(it->hasNext())
    {
        it->next();
        fileName = it->fileInfo().absoluteFilePath().replace(adapter.getSourcePath() + "/", "");

        emit this->newMessage(tr("Restoring ") + fileName);

        adapter.backupFile(fileName);

        emit this->progressValueAdd(it->fileInfo().size());

        trySuspend();
        result = tryStop();
        if(result != BackupEngine::OK) {
            return result;
        }
    }

    emit this->progressMessageChanged(tr("Restore complete."));
    return result;
}

BackupEngine::EngineError BackupEngine::doMainBackup(AbstractTargetAdapter &adapter, BackupTask::VerificationType vType)
{
    BackupEngine::EngineError result = BackupEngine::OK;

    emit this->progressMessageChanged(tr("Creating file table of directory ") + adapter.getSourcePath() + ".");

    QHash<QString, FileInfo> table = adapter.createFileTable(vType == BackupTask::Hash);

    emit this->progressMessageChanged(tr("Backing up files from directory ") + adapter.getSourcePath() + ".");
    emit this->progressMaximumChanged(adapter.getDirSize(table));

    foreach(QString key, table.keys())
    {
        emit this->newMessage(tr("Backing up ") + key);

        adapter.backupFile(key);

        emit this->progressValueAdd(table[key].getSize());

        trySuspend();
        result = tryStop();
        if(result != BackupEngine::OK) {
            return result;
        }
    }

    emit this->progressMessageChanged(tr("Backup complete."));
    return result;
}

BackupEngine::EngineError BackupEngine::doMainRestore(AbstractTargetAdapter &adapter, BackupTask::VerificationType vType)
{
    emit this->progressMessageChanged(tr("Loading file table of directory ") + adapter.getTargetPath() + ".");

    QHash<QString, FileInfo> table = adapter.loadFileTable(true);

    emit this->progressMessageChanged(tr("Restoring files to directory ") + adapter.getTargetPath() + ".");
    emit this->progressMaximumChanged(adapter.getDirSize(table));

    foreach(QString key, table.keys())
    {
        emit this->newMessage(tr("Restoring ") + key);

        adapter.restoreFile(key);

        emit this->progressValueAdd(table[key].getSize());

        trySuspend();
    }

    emit this->progressMessageChanged(tr("Restore complete."));
    return BackupEngine::OK;
}

BackupEngine::EngineError BackupEngine::doDifferentialBackup(AbstractTargetAdapter &adapter,
                                       BackupTask::VerificationType vType)
{  
    //build new hashtable
    emit this->progressMessageChanged(tr("Building new file table."));
    QHash<QString, FileInfo> newTable = adapter.createFileTable(vType == BackupTask::Hash);

    //load old hashtable
    emit this->progressMessageChanged(tr("Loading old file table."));
    QHash<QString, FileInfo> oldTable = adapter.loadFileTable(false);

    emit this->progressMessageChanged(tr("Creating diffs."));
    emit this->progressMaximumChanged(adapter.getDirSize(newTable));

    /* Run through all files */
    foreach(QString key, newTable.keys())
    {
        /* If file is not in old table, do plain backup */
        if(!oldTable.contains(key))
        {
            emit this->newMessage(tr("Backing up  ") + key);

            adapter.backupFile(key);
        }
        /* Else create XDelta diff */
        else if(oldTable.contains(key))
        {
            if(
                (vType == BackupTask::Hash && oldTable[key].getHash() != newTable[key].getHash())
                ||
                (vType == BackupTask::Size && oldTable[key].getSize() != newTable[key].getSize())
              )
            {
                emit this->newMessage(tr("Creating diff of file  ") + key);

                adapter.createDiff(key);
            }
        }

        emit this->progressValueAdd(newTable[key].getSize());
        trySuspend();
    }

    emit this->progressMessageChanged(tr("Backup complete."));
    return BackupEngine::OK;
}

BackupEngine::EngineError BackupEngine::doIncrementalRestore(AbstractTargetAdapter &adapter,
                                                             BackupTask::VerificationType vType)
{
    //load new hashtable
    QHash<QString, FileInfo> newTable = adapter.loadFileTable(false);

    //load old hashtable
    QHash<QString, FileInfo> oldTable = adapter.loadFileTable(true);

    emit this->progressMessageChanged(tr("Restoring files to directory ") + adapter.getTargetPath() + ".");
    emit this->progressMaximumChanged(adapter.getDirSize(newTable));
    /* Run through all files */
    foreach(QString key, newTable.keys())
    {
        emit this->newMessage(tr("Restoring  ") + key);

        /* If file is not in old table, do plain restore */
        if(!oldTable.contains(key))
        {
            adapter.backupFile(key);
        }
        /* Else restore from XDelta diff */
        else if(oldTable.contains(key))
        {
            if(
                (vType == BackupTask::Hash && oldTable[key].getHash() != newTable[key].getHash())
                ||
                (vType == BackupTask::Size && oldTable[key].getSize() != newTable[key].getSize())
              )
            {
                adapter.restoreDiff(key);
            } else {
                adapter.backupFile(key);
            }
        }

        emit this->progressValueAdd(newTable[key].getSize());
        trySuspend();
    }

    emit this->progressMessageChanged(tr("Restore complete."));
    return BackupEngine::OK;
}
